package none.rg.gantt.xmlio;


import none.rg.gantt.model.*;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import javax.xml.xpath.*;
import org.xml.sax.*;
import java.util.*;
import java.io.*;


/**
 * Singleton class used for loading Charts from XML files.
 * @author Rodion Gorkovenko
 */
public class ChartLoader {
    
    
    private static ChartLoader instance = new ChartLoader();
    
    public static final int ERROR_NONE = 0;
    public static final int ERROR_CONFIG = -1;
    public static final int ERROR_PARSE = -2;
    public static final int ERROR_NOCHART = -3;
    
    private int errors;
    
    /* 
     * Map for searching for tasks by their Titles.
     * This is necessary to describe dependencies in XML
     * Now we suppose it is temporary measure, since
     * titles are not required to be unique.
     * Currently it holds also mapping from number of task
     * inside XML file to the parsed Task object.
     */
    private Map<Object, Task> taskMap;
    
    
    private ChartLoader() {
    } // ChartLoader
    
    
    /** calls parsing of the XML from file with given name */
    public Chart parseFile(String name) {
        return parse(name, true);
    } // parseFile
    
    
    /** calls parsing of the XML from text (represented as String) */
    public Chart parseText(String text) {
        return parse(text, false);
    } // parseFile
    
    
    private synchronized Chart parse(String s, boolean isFileName) {
        DocumentBuilder builder;
        Document xml;
        
        errors = ERROR_NONE;
        taskMap = new HashMap<>();
        
        try {
            builder = DocumentBuilderFactory.newInstance()
                .newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            errors = ERROR_CONFIG;
            return null;
        } // catch
        
        try {
            xml = builder.parse(isFileName
                    ? new FileInputStream(s)
                    : new ByteArrayInputStream(s.getBytes()));
        } catch (SAXException | IOException e) {
            errors = ERROR_PARSE;
            return null;
        } // catch
        
        return loadDom(xml);
        
    } // parse
    
    private Chart loadDom(Document dom) {
        XPath xpath = XPathFactory.newInstance().newXPath();
        Chart chart = new Chart();
        
        chart.enableRecalc(false);
        
        try {
            if (xpath.evaluate("/" + Names.TAG_CHART, dom, XPathConstants.NODE) == null) {
                return null;
            } // if
            
            NodeList tasks = (NodeList) xpath.evaluate("/" + Names.TAG_CHART + "/"
                    + Names.TAG_TASK, dom, XPathConstants.NODESET);
            
            loadTasks(chart, tasks);
            
            for (int i = 0; i < tasks.getLength(); i++) {
                NodeList deps = (NodeList) xpath.evaluate(Names.TAG_DEP,
                        tasks.item(i), XPathConstants.NODESET);
                loadDeps(chart, taskMap.get(i), deps);
            } // for
            
            for (int i = 0; i < tasks.getLength(); i++) {
                NodeList children = (NodeList) xpath.evaluate(Names.TAG_CHILD,
                        tasks.item(i), XPathConstants.NODESET);
                
                if (!Group.SPECIAL_TYPE.equals(taskMap.get(i).getSpecialType())) {
                    if (children.getLength() > 0) {
                        errors++;
                    } // if
                    continue;
                } // if
                
                loadChildren(chart, (Group) taskMap.get(i), children);
            } // for
            
        } catch (XPathExpressionException e) {
            throw new Error("Malformed XPath");
        } // catch
        
        chart.enableRecalc(true);
        
        return chart;
    } // loadDom
    
    
    private void loadTasks(Chart chart, NodeList tasks) {
        for (int i = 0; i < tasks.getLength(); i++) {
            Element taskElem = (Element) tasks.item(i);
            String title = fetchElemAttr(taskElem, Names.ATTR_TASK_TITLE);
            String durStr = fetchElemAttr(taskElem, Names.ATTR_TASK_DUR);
            String rubber = fetchElemAttr(taskElem, Names.ATTR_TASK_RUBBER);
            String special = fetchElemAttr(taskElem, Names.ATTR_TASK_SPECIAL);
            int dur;
            Task task;

            if (title == null) {
                errors++;
                continue;
            } // if

            if (Milestone.SPECIAL_TYPE.equals(special)) {
                task = new Milestone(chart, title);
            } else if (Group.SPECIAL_TYPE.equals(special)) {
                task = new Group(chart, title);
            } else {
                if (durStr == null) {
                    errors++;
                    continue;
                } // if
                try {
                    dur = Integer.parseInt(durStr);
                } catch (NumberFormatException e) {
                    errors++;
                    continue;
                } // catch
                task = new Task(chart, title, dur, !Boolean.parseBoolean(rubber));
            } // else
            taskMap.put(task.getTitle(), task);
            taskMap.put(i, task);
            
        } // for
        
    } // loadTasks
    
    
    private void loadDeps(Chart chart, Task task, NodeList deps) {
        
        for (int j = 0; j < deps.getLength(); j++) {
            Element dep = (Element) deps.item(j);
            String pred = fetchElemAttr(dep, Names.ATTR_DEP_PRED);
            String type = fetchElemAttr(dep, Names.ATTR_DEP_TYPE);
            
            if (type == null) {
                type = Names.linkTypesStr.get(Link.Type.FINISH_START);
            } // if
            if (pred == null || !taskMap.containsKey(pred)
                    || !Names.linkTypes.containsKey(type)) {
                errors++;
                continue;
            } // if
            
            chart.addLink(taskMap.get(pred), task, Names.linkTypes.get(type));
        } // for
        
    } // loadDeps
    
    
    private void loadChildren(Chart chart, Group group, NodeList children) {
        
        for (int j = 0; j < children.getLength(); j++) {
            Element child = (Element) children.item(j);
            String title = fetchElemAttr(child, Names.ATTR_TASK_TITLE);
            
            if (title == null) {
                errors++;
                continue;
            } // if
            
            group.includeChild(taskMap.get(title));
        } // for
        
    } // loadChildren
    
    
    private String fetchElemAttr(Element elem, String attr) {
        String value = elem.getAttribute(attr);
        if ("".equals(value) && !elem.hasAttribute(attr)) {
            value = null;
        } // if
        return value;
    } // fetchElemAttr
    
    
    /**
     * Returns error count which is less then 0 in case of fatal problems,
     * or greater then 0 if it counted warnings during reading file.
     */
    public int getErrors() { return errors; }
    
    
    /** Returns instance of a loader. */
    public static ChartLoader getInstance() {
        return instance;
    } // getInstance
    
    
} // class ChartLoader
